Popular Searches
Popular Course Categories
Popular Courses

Connecting APIs with Flutter UI

Connecting APIs with Flutter UI

Flutter APIs & Networking

 


Connecting APIs with Flutter UI


Connecting APIs with Flutter UI means integrating a Flutter application's user interface with backend services so that the application can fetch, display, create, update, and delete dynamic data. A typical Flutter API integration involves making an HTTP request, receiving a JSON response, converting the response into Dart model objects, managing the application state, and displaying the result using Flutter widgets.


The http package is commonly used for HTTP communication, while dart:convert provides JSON encoding and decoding functionality. Flutter's networking documentation demonstrates the workflow of requesting data, converting responses into custom Dart objects, and displaying the results with widgets such as FutureBuilder.

 

 


 

 

1. What Does Connecting an API with Flutter UI Mean?


An API acts as a communication layer between the Flutter application and a backend server. Flutter sends a request, the server processes it, and the server returns data that can be displayed in the application's UI.


Flutter UI
    ↓
User Action
    ↓
API Service
    ↓
HTTP Request
    ↓
Backend Server
    ↓
JSON Response
    ↓
JSON Parsing
    ↓
Dart Model
    ↓
Application State
    ↓
Flutter Widgets
    ↓
Updated UI

 

 

For example, when a user opens a product screen:


User opens Product Screen
        ↓
Flutter calls Product API
        ↓
Server returns JSON
        ↓
Flutter decodes JSON
        ↓
JSON becomes Product objects
        ↓
Product widgets are created
        ↓
User sees products

 

 


 

 

2. Why Connect APIs with Flutter UI?



  • To display dynamic server-side data.

  • To create applications with real-time or frequently updated content.

  • To display user profiles.

  • To display products and categories.

  • To display orders and transactions.

  • To create login and registration systems.

  • To submit forms to a backend.

  • To implement search functionality.

  • To load news, posts, comments, and social feeds.

  • To synchronize application data with a backend database.

 

 


 

 

3. API and UI Architecture


A clean Flutter API integration can be divided into several layers.


UI Layer
    ↓
State / Controller
    ↓
Repository or Service
    ↓
HTTP Client
    ↓
REST API
    ↓
Database

 










Layer Responsibility
UI Displays data and collects user actions.
State Tracks loading, success, error, and application data.
Service Makes HTTP requests and processes API responses.
Repository Provides an abstraction over data sources.
Model Represents structured API data.
API Processes requests and returns data.
Database Stores persistent backend data.

 


 

 

4. Required Packages


The http package provides a straightforward way to make HTTP requests from Flutter applications.


flutter pub add http

 

 

Import the required packages:


import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

 

 

For Android applications, internet access should also be declared in the Android manifest when required:


 

 

 


 

 

5. Understanding the API-to-UI Flow


A complete API-connected Flutter screen normally follows these steps:



  1. User opens a screen or performs an action.

  1. Flutter starts an asynchronous API request.

  1. The application displays a loading state.

  1. The server processes the request.

  1. The server returns a response.

  1. Flutter checks the HTTP status code.

  1. The JSON response is decoded.

  1. The decoded data is converted into Dart model objects.

  1. The application updates its state.

  1. Flutter rebuilds the required widgets.

  1. The user sees the API data.

 

 


 

 

6. Example JSON Response


Suppose an API returns the following user data:


{
  "id": 1,
  "name": "Rahul Sharma",
  "email": "[email protected]"
}

 

 

The Flutter application can convert this JSON into a Dart model.

 

 


 

 

7. Creating a Dart Model


Model classes represent the structure of API data.


class User {
  final int id;
  final String name;
  final String email;

 


  const User({
    required this.id,
    required this.name,
    required this.email,
  });


  factory User.fromJson(Map json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      email: json['email'] as String,
    );
  }
}

 

 

Using a model provides clearer and more strongly typed access to the data:


final user = User.fromJson(json);

 


print(user.name);
print(user.email);

 

 


 

 

8. Creating an API Service


The API service should contain networking logic rather than placing HTTP requests throughout the UI widgets.


class UserService {
  Future fetchUser() async {
    final response = await http.get(
      Uri.parse(
        'https://jsonplaceholder.typicode.com/users/1',
      ),
    );

 


    if (response.statusCode == 200) {
      final data =
          jsonDecode(response.body) as Map;


      return User.fromJson(data);
    }


    throw Exception('Failed to load user');
  }
}

 

 


 

 

9. Calling the API from a Flutter Screen


The screen can use the service to retrieve data.


class UserScreen extends StatefulWidget {
  const UserScreen({super.key});

 


  @override
  State createState() => _UserScreenState();
}


class _UserScreenState extends State {
  final UserService userService = UserService();


  late Future futureUser;


  @override
  void initState() {
    super.initState();
    futureUser = userService.fetchUser();
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('User'),
      ),
      body: const SizedBox(),
    );
  }
}

 

 

Flutter's official networking recipe recommends initiating the Future in initState() or didChangeDependencies() rather than repeatedly starting the request from build().

 

 


 

 

10. Connecting the Future to FutureBuilder


FutureBuilder can connect asynchronous API results to Flutter's widget tree.


FutureBuilder(
  future: futureUser,
  builder: (context, snapshot) {
    if (snapshot.connectionState ==
        ConnectionState.waiting) {
      return const CircularProgressIndicator();
    }

 


    if (snapshot.hasError) {
      return Text('Error: ${snapshot.error}');
    }


    if (snapshot.hasData) {
      final user = snapshot.data!;


      return Text(user.name);
    }


    return const Text('No data available');
  },
)

 

 

This creates a direct connection between the API's asynchronous result and the UI.

 

 


 

 

11. Understanding FutureBuilder States








State Meaning Typical UI
Waiting API request is still running. Loading indicator
Success API returned usable data. Content widgets
Error Request or processing failed. Error message
Empty Request succeeded but contains no records. Empty-state message

 

 


 

 

12. Displaying API Data with Text


if (snapshot.hasData) {
  final user = snapshot.data!;

 


  return Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      Text('ID: ${user.id}'),
      Text('Name: ${user.name}'),
      Text('Email: ${user.email}'),
    ],
  );
}

 

 


 

 

13. Displaying API Data with Card


Card can be used to create a structured UI for API data.


Card(
  margin: const EdgeInsets.all(16),
  child: Padding(
    padding: const EdgeInsets.all(16),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          user.name,
          style: const TextStyle(
            fontSize: 22,
            fontWeight: FontWeight.bold,
          ),
        ),
        const SizedBox(height: 8),
        Text(user.email),
        const SizedBox(height: 8),
        Text('User ID: ${user.id}'),
      ],
    ),
  ),
)

 

 


 

 

14. Connecting a List API with Flutter UI


Many APIs return multiple objects.


[
  {
    "id": 1,
    "name": "Rahul",
    "email": "[email protected]"
  },
  {
    "id": 2,
    "name": "Priya",
    "email": "[email protected]"
  },
  {
    "id": 3,
    "name": "Amit",
    "email": "[email protected]"
  }
]

 

 

Create a service method:


class UserService {
  Future> fetchUsers() async {
    final response = await http.get(
      Uri.parse(
        'https://jsonplaceholder.typicode.com/users',
      ),
    );

 


    if (response.statusCode != 200) {
      throw Exception('Failed to load users');
    }


    final List data = jsonDecode(response.body);


    return data
        .map(
          (item) => User.fromJson(
            item as Map,
          ),
        )
        .toList();
  }
}

 

 


 

 

15. Displaying API List with ListView.builder


FutureBuilder>(
  future: futureUsers,
  builder: (context, snapshot) {
    if (snapshot.connectionState ==
        ConnectionState.waiting) {
      return const Center(
        child: CircularProgressIndicator(),
      );
    }

 


    if (snapshot.hasError) {
      return Center(
        child: Text(
          'Unable to load users',
        ),
      );
    }


    final users = snapshot.data ?? [];


    if (users.isEmpty) {
      return const Center(
        child: Text('No users found'),
      );
    }


    return ListView.builder(
      itemCount: users.length,
      itemBuilder: (context, index) {
        final user = users[index];


        return ListTile(
          leading: CircleAvatar(
            child: Text(user.name[0]),
          ),
          title: Text(user.name),
          subtitle: Text(user.email),
        );
      },
    );
  },
)

 

 


 

 

16. Complete List API Screen


class UsersScreen extends StatefulWidget {
  const UsersScreen({super.key});

 


  @override
  State createState() => _UsersScreenState();
}


class _UsersScreenState extends State {
  final UserService userService = UserService();


  late Future> futureUsers;


  @override
  void initState() {
    super.initState();
    futureUsers = userService.fetchUsers();
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Users'),
      ),
      body: FutureBuilder>(
        future: futureUsers,
        builder: (context, snapshot) {
          if (snapshot.connectionState ==
              ConnectionState.waiting) {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }


          if (snapshot.hasError) {
            return Center(
              child: ElevatedButton(
                onPressed: () {
                  setState(() {
                    futureUsers =
                        userService.fetchUsers();
                  });
                },
                child: const Text('Retry'),
              ),
            );
          }


          final users = snapshot.data ?? [];


          if (users.isEmpty) {
            return const Center(
              child: Text('No users found'),
            );
          }


          return ListView.builder(
            itemCount: users.length,
            itemBuilder: (context, index) {
              final user = users[index];


              return Card(
                margin: const EdgeInsets.symmetric(
                  horizontal: 12,
                  vertical: 6,
                ),
                child: ListTile(
                  leading: CircleAvatar(
                    child: Text(user.name[0]),
                  ),
                  title: Text(user.name),
                  subtitle: Text(user.email),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

 

 


 

 

17. Connecting API Data to a Grid UI


For product catalogs, categories, images, and dashboard cards, API data can be displayed using GridView.builder.


GridView.builder(
  padding: const EdgeInsets.all(12),
  gridDelegate:
      const SliverGridDelegateWithFixedCrossAxisCount(
    crossAxisCount: 2,
    crossAxisSpacing: 12,
    mainAxisSpacing: 12,
    childAspectRatio: 0.75,
  ),
  itemCount: products.length,
  itemBuilder: (context, index) {
    final product = products[index];

 


    return Card(
      child: Padding(
        padding: const EdgeInsets.all(8),
        child: Column(
          crossAxisAlignment:
              CrossAxisAlignment.start,
          children: [
            Expanded(
              child: Image.network(
                product.image,
                width: double.infinity,
                fit: BoxFit.cover,
              ),
            ),
            const SizedBox(height: 8),
            Text(product.name),
            Text('₹${product.price}'),
          ],
        ),
      ),
    );
  },
)

 

 


 

 

18. Displaying API Images


Suppose the API returns an image URL:


{
  "id": 101,
  "name": "Laptop",
  "image": "https://example.com/images/laptop.jpg"
}

 

 

The model can contain an image field:


class Product {
  final int id;
  final String name;
  final String image;

 


  const Product({
    required this.id,
    required this.name,
    required this.image,
  });


  factory Product.fromJson(
    Map json,
  ) {
    return Product(
      id: json['id'] as int,
      name: json['name'] as String,
      image: json['image'] as String,
    );
  }
}

 

 

Display the image:


Image.network(
  product.image,
  width: 120,
  height: 120,
  fit: BoxFit.cover,
)

 

 


 

 

19. Connecting TextField Input with an API


API integration is not limited to fetching data. Flutter UI can collect user input and send it to an API.


final TextEditingController titleController =
    TextEditingController();

 

 

Create a text field:


TextField(
  controller: titleController,
  decoration: const InputDecoration(
    labelText: 'Enter Title',
  ),
)

 

 

Send the entered value when a button is pressed:


ElevatedButton(
  onPressed: () {
    final title = titleController.text;

 


    // Send title to API.
  },
  child: const Text('Submit'),
)

 

 


 

 

20. Sending Data from Flutter UI to API


A POST request can send JSON data from a Flutter form to the backend.


Future createUser(
  String name,
  String email,
) async {
  final response = await http.post(
    Uri.parse('https://example.com/api/users'),
    headers: {
      'Content-Type': 'application/json',
    },
    body: jsonEncode({
      'name': name,
      'email': email,
    }),
  );

 


  if (response.statusCode == 201) {
    print('User created successfully');
  } else {
    throw Exception('Failed to create user');
  }
}

 

 

Flutter's networking documentation demonstrates the same general pattern: encode request data as JSON, send it with http.post(), convert the response into a Dart object, and display the result using Flutter UI.

 

 


 

 

21. Complete Form-to-API Example


class CreateUserScreen extends StatefulWidget {
  const CreateUserScreen({super.key});

 


  @override
  State createState() =>
      _CreateUserScreenState();
}


class _CreateUserScreenState
    extends State {
  final nameController = TextEditingController();
  final emailController = TextEditingController();


  bool isLoading = false;


  Future submitForm() async {
    setState(() {
      isLoading = true;
    });


    try {
      final response = await http.post(
        Uri.parse(
          'https://example.com/api/users',
        ),
        headers: {
          'Content-Type': 'application/json',
        },
        body: jsonEncode({
          'name': nameController.text,
          'email': emailController.text,
        }),
      );


      if (!mounted) return;


      if (response.statusCode == 201) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text('User created successfully'),
          ),
        );
      } else {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text('Failed to create user'),
          ),
        );
      }
    } catch (error) {
      if (!mounted) return;


      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('Network error'),
        ),
      );
    } finally {
      if (mounted) {
        setState(() {
          isLoading = false;
        });
      }
    }
  }


  @override
  void dispose() {
    nameController.dispose();
    emailController.dispose();
    super.dispose();
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Create User'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            TextField(
              controller: nameController,
              decoration: const InputDecoration(
                labelText: 'Name',
              ),
            ),
            TextField(
              controller: emailController,
              decoration: const InputDecoration(
                labelText: 'Email',
              ),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: isLoading ? null : submitForm,
              child: isLoading
                  ? const CircularProgressIndicator()
                  : const Text('Submit'),
            ),
          ],
        ),
      ),
    );
  }
}

 

 


 

 

22. API Loading State


The UI should clearly communicate when an API request is in progress.


if (isLoading) {
  return const Center(
    child: CircularProgressIndicator(),
  );
}

 

 

Loading indicators can include:



  • Circular progress indicators

  • Linear progress indicators

  • Skeleton layouts

  • Shimmer effects

  • Inline progress indicators on buttons

 

 


 

 

23. API Success State


When the request succeeds, the UI should display the returned information.


if (snapshot.hasData) {
  final user = snapshot.data!;

 


  return UserCard(user: user);
}

 

 


 

 

24. API Error State


Network requests can fail because of connectivity problems, server errors, authentication issues, invalid responses, or other conditions.


if (snapshot.hasError) {
  return Center(
    child: Text(
      'Unable to load data. Please try again.',
    ),
  );
}

 

 

User-friendly error messages are generally better than exposing technical exception details directly to end users.

 

 


 

 

25. Empty API State


An API may successfully return an empty list. This is different from a network or server error.


final users = snapshot.data ?? [];

 


if (users.isEmpty) {
  return const Center(
    child: Text('No users available'),
  );
}

 

 


 

 

26. Retry Button


A retry action lets the user initiate the API request again after an error.


ElevatedButton(
  onPressed: () {
    setState(() {
      futureUsers =
          userService.fetchUsers();
    });
  },
  child: const Text('Retry'),
)

 

 


 

 

27. Pull-to-Refresh


RefreshIndicator can connect a pull gesture to a new API request.


RefreshIndicator(
  onRefresh: () async {
    setState(() {
      futureUsers =
          userService.fetchUsers();
    });

 


    await futureUsers;
  },
  child: ListView.builder(
    itemCount: users.length,
    itemBuilder: (context, index) {
      final user = users[index];


      return ListTile(
        title: Text(user.name),
        subtitle: Text(user.email),
      );
    },
  ),
)

 

 


 

 

28. Passing API Data to Another Screen


API data can be passed to a detail screen when a user taps an item.


ListTile(
  title: Text(user.name),
  subtitle: Text(user.email),
  onTap: () {
    Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) => UserDetailScreen(
          user: user,
        ),
      ),
    );
  },
)

 

 

The detail screen can receive the model:


class UserDetailScreen extends StatelessWidget {
  final User user;

 


  const UserDetailScreen({
    super.key,
    required this.user,
  });


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(user.name),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment:
              CrossAxisAlignment.start,
          children: [
            Text(
              user.name,
              style: const TextStyle(
                fontSize: 24,
                fontWeight: FontWeight.bold,
              ),
            ),
            Text(user.email),
          ],
        ),
      ),
    );
  }
}

 

 

Flutter's navigation documentation also demonstrates passing a data object to a detail screen when an item is selected.

 

 


 

 

29. Search API Data from Flutter UI


A search field can be connected to API requests.


final TextEditingController searchController =
    TextEditingController();

 


TextField(
  controller: searchController,
  decoration: const InputDecoration(
    hintText: 'Search products',
    prefixIcon: Icon(Icons.search),
  ),
)

 

 

A query parameter can then be sent to the API:


Future> searchProducts(
  String query,
) async {
  final uri = Uri.https(
    'example.com',
    '/api/products',
    {
      'search': query,
    },
  );

 


  final response = await http.get(uri);


  if (response.statusCode != 200) {
    throw Exception('Search failed');
  }


  final List data = jsonDecode(response.body);


  return data
      .map(
        (item) => Product.fromJson(
          item as Map,
        ),
      )
      .toList();
}

 

 


 

 

30. Filtering API Data Locally


If the application already has a manageable dataset, filtering can be performed locally.


final filteredUsers = users.where((user) {
  return user.name
      .toLowerCase()
      .contains(searchText.toLowerCase());
}).toList();

 

 


 

 

31. Connecting API Data with Forms


Flutter forms can be connected to APIs for operations such as:



  • Login

  • Registration

  • Profile updates

  • Password changes

  • Product creation

  • Order creation

  • Contact forms

  • Search

  • Feedback submission

 

 

The basic flow is:


Form
 ↓
Validation
 ↓
Read Input
 ↓
Create JSON
 ↓
HTTP Request
 ↓
API Response
 ↓
Parse Response
 ↓
Update UI

 

 


 

 

32. API Validation Before Sending Data


Input should be validated before sending data to the backend.


if (nameController.text.trim().isEmpty) {
  return;
}

 


if (!emailController.text.contains('@')) {
  return;
}


await createUser(
  nameController.text.trim(),
  emailController.text.trim(),
);

 

 

Client-side validation improves user experience, while the backend should still validate incoming data independently.

 

 


 

 

33. Authentication and API UI


Protected APIs commonly require an authorization header.


final response = await http.get(
  Uri.parse('https://example.com/api/profile'),
  headers: {
    'Authorization': 'Bearer $token',
    'Accept': 'application/json',
  },
);

 

 

Authentication APIs can be connected to Flutter login forms and then used to load protected application data.

 

 


 

 

34. Handling Authentication Errors


For example, a 401 response can indicate that the request is not authorized.


if (response.statusCode == 401) {
  throw Exception('Authentication required');
}

 

 

The UI can respond by showing a login message or navigating the user to an authentication screen, depending on the application's architecture.

 

 


 

 

35. PUT and PATCH Operations


API-connected Flutter applications can also update existing backend records.


final response = await http.put(
  Uri.parse(
    'https://example.com/api/users/1',
  ),
  headers: {
    'Content-Type': 'application/json',
  },
  body: jsonEncode({
    'name': 'Updated Name',
  }),
);

 

 

Flutter's networking cookbook provides an HTTP PUT example for updating server-side data.

 

 


 

 

36. DELETE Operations


A delete button in the UI can trigger an HTTP DELETE request.


Future deleteUser(int id) async {
  final response = await http.delete(
    Uri.parse(
      'https://example.com/api/users/$id',
    ),
  );

 


  if (response.statusCode != 200 &&
      response.statusCode != 204) {
    throw Exception('Failed to delete user');
  }
}

 

 

After successful deletion, the UI can refresh or update its local state.

 

 


 

 

37. Updating the UI After Delete


await deleteUser(user.id);

 


if (!mounted) return;


setState(() {
  users.removeWhere(
    (item) => item.id == user.id,
  );
});

 

 

This allows the UI to immediately reflect the changed application state.

 

 


 

 

38. Keeping API Logic Outside Widgets


A common architecture separates networking from presentation.


lib/
├── models/
│   └── user.dart
├── services/
│   └── user_service.dart
├── repositories/
│   └── user_repository.dart
├── screens/
│   └── users_screen.dart
├── widgets/
│   └── user_card.dart
└── main.dart

 

 

Model


Represents API data.

 

 

Service


Communicates with the HTTP API.

 

 

Repository


Provides an abstraction for retrieving and storing data.

 

 

Screen


Coordinates the UI.

 

 

Widget


Displays reusable pieces of the interface.

 

 


 

 

39. Repository Pattern Example


class UserRepository {
  final UserService service;

 


  UserRepository(this.service);


  Future> getUsers() {
    return service.fetchUsers();
  }
}

 

 

The UI can depend on the repository rather than directly depending on the HTTP implementation.

 

 


 

 

40. State Management for API Data


For small screens, FutureBuilder or setState may be sufficient. Larger applications may use a dedicated state-management architecture.

 

 

Common approaches include:



  • setState

  • FutureBuilder

  • ChangeNotifier

  • Provider

  • Riverpod

  • Bloc/Cubit

  • Other application-specific state-management solutions

 

 


 

 

41. API State Object


A reusable state representation can make API screens easier to manage.


enum ApiStatus {
  initial,
  loading,
  success,
  empty,
  error,
}

 

 

A state class can contain the current status and data:


class UserState {
  final ApiStatus status;
  final List users;
  final String? errorMessage;

 


  const UserState({
    this.status = ApiStatus.initial,
    this.users = const [],
    this.errorMessage,
  });
}

 

 


 

 

42. Responsive API UI


API data should be displayed appropriately across different screen sizes.


For example:



  • Mobile can use a single-column list.

  • Tablet can use a multi-column grid.

  • Desktop can use a wider grid or data table.

 

 

final width = MediaQuery.sizeOf(context).width;

 


if (width < 600) {
  // Mobile layout
} else {
  // Larger-screen layout
}

 

 

The important point is that API data and UI layout should remain separate. The same model data can be presented differently depending on available screen space.

 

 


 

 

43. Pagination with API UI


Large datasets should generally be loaded in smaller pages rather than downloading every record at once.


GET /api/products?page=1&limit=20
GET /api/products?page=2&limit=20

 

 

A Flutter application can load the next page when the user approaches the end of the list.


if (index == products.length - 1) {
  loadNextPage();
}

 

 


 

 

44. API Data Caching


Caching can reduce unnecessary API requests and improve perceived performance. Depending on the application, cached data may be stored in memory or persistent local storage.

 

 

A common approach is:


Check Cache
    ↓
Data Available?
    ↓
Yes → Display Cached Data
    ↓
Refresh from API
    ↓
Update Cache
    ↓
Update UI

 

 


 

 

45. Background JSON Parsing


Large JSON responses can require significant processing. Flutter documentation notes that expensive JSON parsing can cause UI jank when it takes substantial time, and such work can be moved to another isolate.


List parseUsers(String responseBody) {
  final List data = jsonDecode(responseBody);

 


  return data
      .map(
        (item) => User.fromJson(
          item as Map,
        ),
      )
      .toList();
}

 

 

For sufficiently large JSON responses, this parser can be used with compute().


final users = await compute(
  parseUsers,
  response.body,
);

 

 


 

 

46. Complete API-to-UI Architecture Example


class UserService {
  Future> fetchUsers() async {
    final response = await http.get(
      Uri.parse(
        'https://jsonplaceholder.typicode.com/users',
      ),
    );

 


    if (response.statusCode != 200) {
      throw Exception('Failed to load users');
    }


    final List data = jsonDecode(response.body);


    return data
        .map(
          (item) => User.fromJson(
            item as Map,
          ),
        )
        .toList();
  }
}


class UsersScreen extends StatefulWidget {
  const UsersScreen({super.key});


  @override
  State createState() =>
      _UsersScreenState();
}


class _UsersScreenState extends State {
  final UserService service = UserService();


  late Future> futureUsers;


  @override
  void initState() {
    super.initState();
    futureUsers = service.fetchUsers();
  }


  void reloadUsers() {
    setState(() {
      futureUsers = service.fetchUsers();
    });
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Users'),
        actions: [
          IconButton(
            onPressed: reloadUsers,
            icon: const Icon(Icons.refresh),
          ),
        ],
      ),
      body: FutureBuilder>(
        future: futureUsers,
        builder: (context, snapshot) {
          if (snapshot.connectionState ==
              ConnectionState.waiting) {
            return const Center(
              child: CircularProgressIndicator(),
            );
          }


          if (snapshot.hasError) {
            return Center(
              child: Column(
                mainAxisAlignment:
                    MainAxisAlignment.center,
                children: [
                  const Text(
                    'Unable to load users',
                  ),
                  const SizedBox(height: 12),
                  ElevatedButton(
                    onPressed: reloadUsers,
                    child: const Text('Retry'),
                  ),
                ],
              ),
            );
          }


          final users = snapshot.data ?? [];


          if (users.isEmpty) {
            return const Center(
              child: Text('No users found'),
            );
          }


          return RefreshIndicator(
            onRefresh: () async {
              final future = service.fetchUsers();


              setState(() {
                futureUsers = future;
              });


              await future;
            },
            child: ListView.builder(
              itemCount: users.length,
              itemBuilder: (context, index) {
                final user = users[index];


                return ListTile(
                  leading: CircleAvatar(
                    child: Text(user.name[0]),
                  ),
                  title: Text(user.name),
                  subtitle: Text(user.email),
                  onTap: () {
                    Navigator.push(
                      context,
                      MaterialPageRoute(
                        builder: (context) =>
                            UserDetailScreen(
                          user: user,
                        ),
                      ),
                    );
                  },
                );
              },
            ),
          );
        },
      ),
    );
  }
}

 

 


 

 

47. Common API-to-UI Mistakes

 

 

Mistake 1: Calling API Inside build()


@override
Widget build(BuildContext context) {
  fetchUsers();
  return const SizedBox();
}

This can cause repeated API requests whenever the widget rebuilds.

 

 

Mistake 2: Ignoring the HTTP Status Code


Always verify whether the server returned a successful response before processing the expected data.

 

 

Mistake 3: Not Handling Loading


Users need visual feedback while waiting for network operations.

 

 

Mistake 4: Treating Empty Data as an Error


An empty list may represent a valid successful response.

 

 

Mistake 5: Putting All API Logic in Widgets


Large widgets become difficult to maintain when networking, parsing, state management, and UI are all mixed together.

 

 

Mistake 6: Not Handling Network Errors


Temporary connectivity problems are normal in mobile applications. Provide a useful error and retry experience.

 

 

Mistake 7: Loading Very Large Datasets at Once


Use pagination, filtering, caching, and efficient parsing when dealing with large datasets.

 

 


 

 

48. Best Practices



  • Use a dedicated API service or repository layer.

  • Use Dart model classes for structured API responses.

  • Check HTTP status codes.

  • Decode JSON safely.

  • Keep networking code separate from presentation code.

  • Do not make API calls directly inside build().

  • Manage loading, success, empty, and error states.

  • Use FutureBuilder for simple Future-based screens.

  • Use appropriate state management for complex applications.

  • Use ListView.builder for dynamic lists.

  • Use GridView.builder for dynamic grids.

  • Use pagination for large datasets.

  • Provide retry and refresh functionality.

  • Validate form input before sending it to the API.

  • Keep authentication information secure.

  • Use reusable widgets for repeated API-driven UI.

  • Consider background JSON parsing for sufficiently large responses.

  • Handle nullable and unexpected API fields safely.

 

 


 

 

49. Practical Mini Project: Product API App


Create a Flutter application that connects a product API with the UI.

 

 

Features



  • Product API integration

  • Product model

  • Product service

  • Product list

  • Product images

  • Product names

  • Product prices

  • Loading indicator

  • Error handling

  • Retry button

  • Pull-to-refresh

  • Search products

  • Product detail screen

  • Responsive product grid

 

 

Suggested Structure


lib/
├── models/
│   └── product.dart
├── services/
│   └── product_service.dart
├── screens/
│   ├── products_screen.dart
│   └── product_detail_screen.dart
├── widgets/
│   └── product_card.dart
└── main.dart

 

 


 

 

50. Practice Exercise


Build a Flutter user-management application connected to a REST API.



  1. Create a User model.

  1. Create a UserService.

  1. Fetch users with HTTP GET.

  1. Decode the JSON response.

  1. Display users in a ListView.builder.

  1. Create a user detail screen.

  1. Pass the selected user to the detail screen.

  1. Add a search field.

  1. Add loading and error states.

  1. Add a retry button.

  1. Add pull-to-refresh.

  1. Create a registration form.

  1. Send registration data using POST.

  1. Display the API response after registration.

  1. Add appropriate validation and user feedback.

 

 


 

 

51. Interview Questions



  1. What does API integration mean in Flutter?

  1. How does Flutter communicate with a REST API?

  1. What package is commonly used for HTTP requests?

  1. Why is dart:convert used in API integration?

  1. What is a model class?

  1. Why should API responses be converted into model objects?

  1. What is the role of a service class?

  1. What is FutureBuilder?

  1. Why should an API request not normally be called directly inside build()?

  1. How do you display an API list using ListView.builder?

  1. How do you display API images?

  1. How do you handle API loading states?

  1. How do you handle API errors?

  1. How do you implement retry functionality?

  1. How can API data be refreshed?

  1. How can API data be passed to another screen?

  1. How do you send form data to an API?

  1. What is the purpose of an authorization header?

  1. How do you implement pagination?

  1. How can large JSON responses be parsed efficiently?

 

 


 

 

52. Quick Revision


















Concept Purpose
API Provides communication between Flutter and backend services.
http.get() Fetches data from an API.
http.post() Sends data to an API.
http.put() Updates data through an API.
http.delete() Deletes server-side data.
jsonDecode() Converts JSON text into Dart data.
jsonEncode() Converts Dart data into JSON text.
Model Represents structured API data.
Service Handles API communication.
FutureBuilder Connects Future-based asynchronous data with the UI.
ListView.builder Displays dynamic API lists.
GridView.builder Displays dynamic API data in a grid.
RefreshIndicator Provides pull-to-refresh behavior.
compute() Can move expensive parsing work to another isolate.

 

 


 

 

53. Useful Resources












 

 


 

 

Conclusion


Connecting APIs with Flutter UI creates a bridge between dynamic backend data and the application's visual interface. The complete process involves creating an API request, receiving the response, decoding JSON, converting the response into model objects, managing asynchronous state, and displaying the data through Flutter widgets.


A well-structured Flutter application keeps networking, models, state management, and UI responsibilities separated. For simple applications, FutureBuilder can connect a Future directly to the UI. For larger applications, service or repository layers, dedicated state management, pagination, caching, reusable widgets, and background JSON parsing can provide a more scalable architecture.

 

whatsapp